package com.qf.qfwemedia.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qf.qfleadnewscommons.minio.MinioOperator;
import com.qf.qfleadnewscommons.mvc.ThreadLocalUtils;
import com.qf.qfleadnewscommons.security.SecurityScanOperator;
import com.qf.qfleadnewscommons.security.SuggestionResult;
import com.qf.qfleadnewsfeignapi.schedule.ScheduleApi;
import com.qf.qfleadnewsmodel.commons.consts.KafkaConst;
import com.qf.qfleadnewsmodel.commons.dtos.ResponseResult;
import com.qf.qfleadnewsmodel.enums.AppHttpCodeEnum;
import com.qf.qfleadnewsmodel.enums.TaskTypeEnum;
import com.qf.qfleadnewsmodel.schedule.dtos.Task;
import com.qf.qfleadnewsmodel.wemedia.dtos.NewsDto;
import com.qf.qfleadnewsmodel.wemedia.pojos.WmMaterial;
import com.qf.qfleadnewsmodel.wemedia.pojos.WmNews;
import com.qf.qfleadnewsmodel.wemedia.pojos.WmNewsMaterial;
import com.qf.qfleadnewsutils.commons.ProtostuffUtil;
import com.qf.qfwemedia.mapper.WmMaterialMapper;
import com.qf.qfwemedia.mapper.WmNewsMapper;
import com.qf.qfwemedia.mapper.WmNewsMaterialMapper;
import com.qf.qfwemedia.service.WmNewsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
@Slf4j
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {

    @Autowired
    private WmNewsMaterialMapper wmNewsMaterialMapper;

    @Autowired
    private WmMaterialMapper wmMaterialMapper;

    @Autowired
    private SecurityScanOperator securityScanOperator;

    @Autowired
    private MinioOperator minioOperator;

    //获取当前项目中的spring容器。通过spring容器获取对象。springboot的高版本中，将spring的循环依赖功能禁止了
    //如果想要使用spring的循环依赖，需要额外开启支持。
    @Autowired
    private ApplicationContext ioc;

    @Autowired
    private ScheduleApi scheduleApi;

    @Override
    @Transactional
    public ResponseResult submit(NewsDto newsDto) throws UnsupportedEncodingException {
        //健壮性判断
        if (newsDto == null){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        String title = newsDto.getTitle();
        String content = newsDto.getContent();

        if (StringUtils.isBlank(title) || StringUtils.isBlank(content)){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        //参数校验通过
        WmNews wmNews = new WmNews();
        //将NewsDto中的数据拷贝到WmNews中
        BeanUtils.copyProperties(newsDto,wmNews);

        //对images字段进行修复（newsDto-List<String>  , wmNews - String. 多张图片间使用,隔开 ）
        List<String> images = newsDto.getImages();
        if (images != null && images.size()>0){
            //将List<String>转换为String,多个图片间用,隔开
            String imagesStr = images.stream().collect(Collectors.joining(","));
            wmNews.setImages(imagesStr);
        }


        //填充用户id
        Long uid = ThreadLocalUtils.getUid();
        wmNews.setUserId(uid.intValue());

        //填充创建时间和提交时间
        //编辑文章后，提交过来的信息应该带文章的id，新增文章，提交过来的信息没有文章id的
        if (wmNews.getId() == null) {
            wmNews.setCreatedTime(new Date()); //文章第一次创建的时间(如何区分是否为第一次???)
        }

        wmNews.setSubmitedTime(new Date()); //文章每次放生改变后，都会更新的一个时间

        if (wmNews.getPublishTime() == null) {
            //使用当前系统时间作为文章的发布时间
            wmNews.setPublishTime(new Date());
        }

        //填充enable(表示文章的启用状态，默认是1-启用 0-禁用)
        wmNews.setEnable((short) 1);

        //如果封面图片是自动类型，我们在这里直接设置为无图
        //todo：后续提交审核时，处理封面图片
        if (newsDto.getType() == -1){
            wmNews.setType((short) 0);
        }

        log.info("submit:wmNews:" + wmNews);

        //保存记录到wmnews表中(这里做的有可能是新增文章-wmnews没有id，也可能是修改文章-wmnew有id)
//        save(wmNews);
        saveOrUpdate(wmNews);

        //如果保存草稿，程序到此结束，提示保存成功。
        if (wmNews.getStatus() == 0){
            return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
        }

        //保存文章和素材的关系
        handleNewsAndMaterilRelation(wmNews,newsDto);

        //做文章内容的审核
        //ioc.getBean(WmNewsService.class).autoScan(wmNews);

        //延迟发布
        ioc.getBean(WmNewsService.class).delayPub(wmNews);

        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Async
    public void delayPub(WmNews wmNews) {
        //将发布任务添加到任务调度服务
        Task task = new Task();

        //设置为发布文章的任务类型
        task.setTaskType(TaskTypeEnum.NEWS_SCAN_TIME.getTaskType());
        //设置为发布文章的优先级
        task.setPriority(TaskTypeEnum.NEWS_SCAN_TIME.getPriority());
        //任务的执行时间为文章的发布时间
        task.setExecuteTime(wmNews.getPublishTime().getTime());
        //任务参数（为了防止数据量过大，将文章的id作为任务参数）
        Integer id = wmNews.getId();

        //ProtostuffUtil内部通过反射方式构建对象，利用的是空参构造，Integer是没有这个空参构造。进行反序列化时就会报错。这类我们修改为String
        byte[] bys = ProtostuffUtil.serialize(id+"");
        task.setParameters(bys);

        scheduleApi.addTask(task);
    }

    /**
     * 文章内容和图片自动审核
     * 思考：如果自动审核出现了异常，之前保存的文章信息要不要回滚？？？
     *      不需要，所以自动审核的操作和上述文章保存操作应该是在不同事务中
     * 思考：审核操作需要同步执行吗？？？
     *      不需要的，只要文章数据写入到news表中，审核可以在后台通过异步线程慢慢做
     * 思考： 我们在一个非异步的方法中嵌套调用@Async修饰的异步方法时，如果使用this调用，也会像
     *      之前事务失效一样，导致异步失效。所以我们不能使用this调用，需要使用代理对象调用
     * @param wmNews
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    //让当前方法在异步线程中执行
    @Async
    public ResponseResult autoScan(WmNews wmNews) throws UnsupportedEncodingException {
       /* //审核文本
        String title = wmNews.getTitle();
        String content = fetchDataByType(wmNews.getContent(),"text").stream().collect(Collectors.joining(","));
        //调用安全api完成文本审核
        List<SuggestionResult> results = securityScanOperator.textScan(title + content);

        boolean textRes = handlerScanResult(wmNews,results);
        if (!textRes){
            return ResponseResult.errorResult(AppHttpCodeEnum.NEWS_SCAN_FAIL);
        }

        //文本通过

        //审核图片
        List<String> contentImages = fetchDataByType(wmNews.getContent(), "image");
        String images = wmNews.getImages();
        List<String> fmImages = new ArrayList<>();
        if (!StringUtils.isBlank(images)){
            fmImages = Arrays.asList(images.split(","));
        }

        //获取去重后的图片地址
        List<String> distinctImages =
                        //将多个stream流合并（要求stream流的类型必须一致）
                        Stream.concat(contentImages.stream(), fmImages.stream())
                        //去重
                        .distinct()
                        .collect(Collectors.toList());

        for (String imageUrl : distinctImages) {
            byte[] bytes = minioOperator.downLoadFile(imageUrl);
            List<SuggestionResult> imageResults = securityScanOperator.imageScan(bytes);

            boolean imageRes = handlerScanResult(wmNews, imageResults);

            if (!imageRes){
                return ResponseResult.errorResult(AppHttpCodeEnum.NEWS_SCAN_FAIL);
            }
        }*/

        //文本审核 + 图片审核通过
        log.info("====================发布文章，审核通过,文章id为：{}",wmNews.getId());
        wmNews.setStatus((short) 8);
        wmNews.setReason("审核通过");

        updateById(wmNews);

        //将自媒体服务的文章和文章服务中的数据进行同步
        ioc.getBean(WmNewsService.class).publishNews(wmNews);

        return ResponseResult.errorResult(AppHttpCodeEnum.NEWS_SCAN_SUCCESS);
    }


    @Autowired
    private KafkaTemplate kafkaTemplate;

    /**
     * 文章发布不应该影响文章加入news表，也不应该影响审核结果，所以必须在独立的事务中
     * @param wmNews
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Async
    public void publishNews(WmNews wmNews) {
        log.info("====================发布文章，发送kafka消息开始，文章id为：{}",wmNews.getId());

        sendNewsId(wmNews.getId());

        log.info("====================发布文章，发送kafka消息完成，文章id为：{}",wmNews.getId());
    }

    private void sendNewsId(Integer newsId){
        try {
            kafkaTemplate.send(KafkaConst.NEWS_PUBLISH_TOPIC, newsId+"");
        }catch (Exception e){
            e.printStackTrace();
            //失败重发-控制次数 （redis的incr控制次数，次数到达限制存入重试表）
            sendNewsId(newsId);
        }
    }

    /**
     * 处理审核结果
     * @param wmNews
     * @param results
     * @return
     */
    private boolean handlerScanResult(WmNews wmNews, List<SuggestionResult> results) {
        for (SuggestionResult result : results) {
            String suggestion = result.getSuggestion();
            if ("block".equals(suggestion)){
                //失败 -- 获取失败的原因
                String label = result.getLabel();

                //更新数据库中对应文章的审核状态
                wmNews.setStatus((short) 2);
                wmNews.setReason(label);

                updateById(wmNews);

                //后续动作不用做了
                return false;
            }else if("review".equals(suggestion)){
                //需要人工介入 -- 获取失败的原因
                String label = result.getLabel();

                //更新数据库中对应文章的审核状态
                wmNews.setStatus((short) 3);
                wmNews.setReason(label);

                updateById(wmNews);

                //后续动作不用做了
                return false;
            }
        }
        return true;
    }

    /**
     * 处理文章表图片和素材表的关系
     * @param wmNews
     * @param newsDto
     */
    private void handleNewsAndMaterilRelation(WmNews wmNews, NewsDto newsDto) {
        //将原本news和素材表的关系删除(根据news的id删除)
        Wrapper<WmNewsMaterial> qw = Wrappers.lambdaQuery(WmNewsMaterial.class)
                        .eq(WmNewsMaterial::getNewsId,wmNews.getId());
        wmNewsMaterialMapper.delete(qw);

        //内容中的素材和素材表的关系
        //[{"type":"text","value":"2207-测试12207-测试12207-测试12207-测试12207-测试12207-测试12207-测试1"},
        // {"type":"image","value":"http://localhost:9005/leadnews/2023/04/10/df35d3ef-c16c-463a-ae53-bb959b428f15.jpg"},
        // {"type":"text","value":"2207-测试12207-测试12207-测试12207-测试12207-测试1"},
        // {"type":"image","value":"http://localhost:9005/leadnews/2023/04/10/697d484c-fe3e-4feb-89e6-5d277a22040a.jpg"},
        // {"type":"image","value":"http://localhost:9005/leadnews/2023/02/14/27089f12155d40ea89e2090ec6d874fb.jpg"},
        // {"type":"text","value":"请在这里输入正文"}]
        String content = wmNews.getContent();
        //内容图片
        List<String> contentImages = fetchDataByType(content, "image");

        //保存内容图片和素材表的关系
        saveNewsAndMaterialRelation(wmNews, contentImages,0);

        //封面图片素材和素材表的关系(从页面上传过来的封面图片，可能有，可能没有)
        List<String> images = newsDto.getImages();

        //封面图片自动类型-需要从内容中提取图片作为封面图片
        if (newsDto.getType() == -1){
            int size = contentImages.size();
            if(size >= 3){
                //提取三张作为封面图片
                images = contentImages.stream().limit(3).collect(Collectors.toList());
                wmNews.setType((short) 3);
            }else if (size>=1){
                //提取一张作为封面图片
                images = contentImages.stream().limit(1).collect(Collectors.toList());
                wmNews.setType((short) 1);
            }

            //如果从内容中提取了图片，那么我们要更新news表中的数据
            if (images != null && images.size() > 0){
                String imagesStr = images.stream().collect(Collectors.joining(","));
                wmNews.setImages(imagesStr);
                updateById(wmNews);
            }
        }

        //维护封面图片和素材表的关系
        saveNewsAndMaterialRelation(wmNews,images,1);


    }

    /**
     * 保存新闻和素材的关系
     * @param wmNews 新闻，可以获取新闻id
     * @param contentImages 素材图片集合
     * @param type 素材类型 0-内容素材，1-封面素材
     */
    private void saveNewsAndMaterialRelation(WmNews wmNews, List<String> contentImages,int type) {
        if (contentImages == null){
            return;
        }

        log.info("素材images:" + contentImages);

        //todo: 可以使用线程池+门闩对象进行并发查询优化
        List<WmNewsMaterial> wmNewsMaterialList = new ArrayList<>();
        for (int i = 0; i < contentImages.size(); i++) {
            WmNewsMaterial wmNewsMaterial = new WmNewsMaterial();
            //设置文章id
            wmNewsMaterial.setNewsId(wmNews.getId());

            //根据图片地址获取图片对应的素材id
            Wrapper<WmMaterial> materialWrapper = Wrappers.lambdaQuery(WmMaterial.class)
                    .eq(WmMaterial::getUrl, contentImages.get(i));
            WmMaterial wmMaterial = wmMaterialMapper.selectOne(materialWrapper);
            //设置素材id
            wmNewsMaterial.setMaterialId(wmMaterial.getId());
            //设置类型 0-内容素材，1-封面素材
            wmNewsMaterial.setType((short) type);
            //设置素材的顺序号
            wmNewsMaterial.setOrd((short) i);

            wmNewsMaterialList.add(wmNewsMaterial);
        }

        //新闻素材关系批量添加
        wmNewsMaterialMapper.batchInsert(wmNewsMaterialList);
    }

    /**
     * 从文章内容中根据需要的类型，提取需要的数据
     * @param content
     * @return
     */
    private List<String> fetchDataByType(String content,String needType) {
        List<Map> maps = JSON.parseArray(content, Map.class);
        return  maps
                .stream()
                //过滤方法，这个方法中返回值为true的数据保留，返回为false数据剔除
                .filter(map->{
                    String type = map.get("type").toString();
                    if (type.equals(needType)){
                        return true;
                    }
                    return false;
                })
                //将剩下的map类型的流数据转成string类型的流数据
                .map(map->{
                    return map.get("value").toString();
                }).collect(Collectors.toList());

    }

    public static void main(String[] args) {
        List<String> strings = Arrays.asList("abc", "aaa", "ccc");
        String collect = strings.stream().collect(Collectors.joining(","));
        System.out.println(collect);
    }
}
